//
// SoapTrace.Net - A soap message monitoring utility. 
//
// Copyright (C) 2005 Matthew Ward
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//
// Matthew Ward (mrward@users.sourceforge.net)

using System;
using System.Diagnostics;
using System.Net;
using WinPcap;

namespace SoapTrace.Core.Communication
{
	/// <summary>
	/// Event handler for the <see cref="TcpPacketMonitor.ClientConnected"/> event. 
	/// </summary>
	public delegate void TcpCommunicationEventHandler( object sender, TcpCommunicationEventArgs e );
	
	/// <summary>
	/// Monitors the Tcp packets and presents a high level wrapper around the 
	/// underlying packets.
	/// </summary>
	/// <remarks>
	/// Turns the tcp packets into connection/disconnection/bytes received
	/// events.  Handles packet retransmission.</remarks>
	public class TcpPacketMonitor
	{
		/// <summary>
		/// Client connected event.
		/// </summary>
		public event TcpCommunicationEventHandler ClientConnected;
		
		/// <summary>
		/// Client disconnected event.
		/// </summary>
		public event TcpCommunicationEventHandler ClientDisconnected;
		
		/// <summary>
		/// Bytes transferred event.
		/// </summary>
		public event TcpCommunicationEventHandler BytesTransferred;
		
		/// <summary>
		/// Triggered when we receive data from a client that we did
		/// not see the connection data packets.
		/// </summary>
		public event TcpCommunicationEventHandler MissedConnectionBytesTransferred;		
					
		/// <summary>
		/// Triggered when the missed connection client disconnects.
		/// </summary>
		public event TcpCommunicationEventHandler MissedConnectionDisconnected;
		
		/// <summary>
		/// The tcp packet filter.
		/// </summary>
		private TcpPacketFilter m_PacketFilter;
		
		/// <summary>
		/// The active tcp connections.
		/// </summary>
		private TcpConnectionCollection m_Connections = new TcpConnectionCollection( );
				
		/// <summary>
		/// Creates a new instance of the <see cref="TcpPacketMonitor"/>.
		/// </summary>
		public TcpPacketMonitor( )
		{
		}
		
		/// <summary>
		/// Creates a new instance of the <see cref="TcpPacketMonitor"/>.
		/// </summary>
		/// <param name="address">The IP address of the remote server that 
		/// will be used to filter the packets monitored.</param>
		/// <param name="port">The port of the remote server.</param>
		public TcpPacketMonitor( string address, int port )
		{
			m_PacketFilter = new TcpPacketFilter( address, port );
		}
		
		/// <summary>
		/// Gets whether filtering is enabled.
		/// </summary>
		public bool IsFilterEnabled
		{
			get
			{
				bool Enabled = false;
				
				if( m_PacketFilter != null )
				{
					Enabled = true;
				}
				
				return Enabled;
			}
		}
		
		/// <summary>
		/// Changes the filter.
		/// </summary>
		public void SetFilter( string address, int port )
		{			
			if( IsFilterMatch( address, port ) )
			{
				// No need to change the filter.	
			}
			else
			{
				Clear( );
				m_PacketFilter = new TcpPacketFilter( address, port );
			}
		}
		
		/// <summary>
		/// Checks whether the filter matches the one currently in use.
		/// </summary>
		public bool IsFilterMatch( string address, int port )
		{
			bool IsMatch = false;
			
			if( m_PacketFilter != null )
			{
				if( ( m_PacketFilter.IPAddress == address ) &&
					( m_PacketFilter.Port == port ) )
				{
					IsMatch = true;
				}
			}
			
			return IsMatch;
		}
		
		/// <summary>
		/// Gets the IP address and port string of the filter currently
		/// being used.
		/// </summary>
		public string RemoteEndPointFilter
		{
			get
			{
				string EndPoint = String.Empty;
				
				if( m_PacketFilter != null )
				{
					EndPoint = m_PacketFilter.RemoteEndPoint;	
				}
				
				return EndPoint;
			}
		}
		
		/// <summary>
		/// Clears the connected clients.
		/// </summary>
		public void Clear( )
		{
			lock( m_Connections.SyncRoot )
			{
				m_Connections.Clear( );
			}
		}
		
		/// <summary>
		/// Processes the packet.
		/// </summary>
		/// <param name="packet">The tcp packet to process.</param>
		public void ProcessPacket( Packet packet )
		{
			TcpPacket TcpPacket = GetFilteredTcpPacket( packet );
			
			ProcessTcpPacket( TcpPacket );
		}	
		
		/// <summary>
		/// Processes the IP packet.
		/// </summary>
		/// <param name="packet">The IP packet to process.</param>
		public void ProcessIPPacket( IPPacket packet )
		{
			TcpPacket TcpPacket = GetFilteredTcpPacket( packet );
			ProcessTcpPacket( TcpPacket );
		}

		/// <summary>
		/// Processes a TCP packet.
		/// </summary>
		/// <param name="packet">A TCP packet</param>
		public void ProcessTcpPacket( TcpPacket packet )
		{
			if( packet != null )
			{
				TcpConnection CurrentConnection =
					m_Connections.FindConnection( packet );
				
				if( CurrentConnection != null )
				{
					CurrentConnection.ProcessPacket( packet );								
				}
				else if( TcpConnection.IsSynPacket( packet ) )
				{
					// New connection attempt.
					
					CreateNewTcpConnection( packet.SourceAddress, 0, packet.DestinationAddress, 0, TcpState.SynSent );
				}
				else if( IsMissedConnectionBytesWanted )
				{
					if( IsFilterEnabled )
					{
						// New client, but we missed the connection packets.
						
						ProcessMissedConnection( packet );
					}
				}
			}
		}
		
		/// <summary>
		/// Raises the <see cref="BytesTransferred"/> event.
		/// </summary>
		protected void OnBytesTransferred( object source, TcpCommunicationEventArgs e )
		{
			if( BytesTransferred != null )
			{				
				BytesTransferred( this, e );
			}
		}	
		
		/// <summary>
		/// Raises the <see cref="MissedConnectionBytesTransferred"/> event.
		/// </summary>
		protected void OnMissedConnectionBytesTransferred( object source, TcpCommunicationEventArgs e )
		{
			if( MissedConnectionBytesTransferred != null )
			{				
				MissedConnectionBytesTransferred( this, e );
			}
		}		
		
		/// <summary>
		/// Raises the <see cref="MissedConnectionDisconnected"/> event.
		/// </summary>
		protected void OnMissedConnectionDisconnected( object source, TcpCommunicationEventArgs e )
		{
			if( MissedConnectionDisconnected != null )
			{				
				MissedConnectionDisconnected( this, e );
			}
		}				
		
		/// <summary>
		/// Raises the <see cref="ClientConnected"/> event.
		/// </summary>
		protected void OnClientConnected( object source, TcpCommunicationEventArgs e )
		{
			if( ClientConnected != null )
			{
				ClientConnected( this, e );
			}
		}
		
		/// <summary>
		/// Raises the <see cref="ClientDisconnected"/> event.
		/// </summary>
		protected void OnClientDisconnected( object source, TcpCommunicationEventArgs e )
		{
			// Remove connection.
			TcpConnection Connection = ( TcpConnection )source;
			
			lock( m_Connections.SyncRoot )
			{
				m_Connections.Remove( Connection );
			}
			
			// Raise the event.
			if( ClientDisconnected != null )
			{
				ClientDisconnected( this, e );
			}
		}
	
		/// <summary>
		/// Gets the Tcp Packet that matches the filter currently in use.
		/// </summary>
		/// <param name="packet">A data packet.</param>
		/// <returns>A <see cref="TcpPacket"/> if the packet matches the
		/// filter or no filter is being used; otherwise <see langword="null"/>.
		/// </returns>
		private TcpPacket GetFilteredTcpPacket( Packet packet )
		{
			TcpPacket TcpPacket = null;
						
			if( m_PacketFilter != null )
			{
				TcpPacket = m_PacketFilter.GetTcpPacket( packet );
			}
			else
			{
				TcpPacket = TcpPacket.GetTcpPacket( packet );
			}
			
			return TcpPacket;
		}
		
		/// <summary>
		/// Gets the Tcp Packet that matches the filter currently in use.
		/// </summary>
		/// <param name="packet">An IP data packet.</param>
		/// <returns>A <see cref="TcpPacket"/> if the packet matches the
		/// filter or no filter is being used; otherwise <see langword="null"/>.
		/// </returns>
		private TcpPacket GetFilteredTcpPacket( IPPacket packet )
		{
			TcpPacket TcpPacket = null;
						
			if( m_PacketFilter != null )
			{
				TcpPacket = m_PacketFilter.GetTcpPacket( packet );
			}
			else
			{
				TcpPacket = ( TcpPacket )( packet.InnerPacket );
			}
			
			return TcpPacket;
		}		
		
		/// <summary>
		/// Creates a new <see cref="TcpConnection"/>.
		/// </summary>
		/// <param name="client">The client IP address.</param>
		/// <param name="clientSequenceNumber">The client's next sequence 
		/// number.</param>
		/// <param name="server">The server IP address.</param>
		/// <param name="serverSequenceNumber">The server's next sequence
		/// number.</param>
		/// <param name="initialState">Initial connection state.</param>
		/// <returns>A new Tcp connection.</returns>
		private TcpConnection CreateNewTcpConnection( IPEndPoint client, 
		                                              uint clientSequenceNumber, 
		                                              IPEndPoint server, 
		                                              uint serverSequenceNumber, 
		                                              TcpState initialState )
		{
			TcpConnection NewConnection = new TcpConnection( client, clientSequenceNumber, server, serverSequenceNumber );
			
			// Hook up events.
			NewConnection.BytesTransferred += new TcpCommunicationEventHandler( OnBytesTransferred );
			NewConnection.Connected += new TcpCommunicationEventHandler( OnClientConnected );
			NewConnection.Disconnected += new TcpCommunicationEventHandler( OnClientDisconnected );
			
			// Set initial connection state.
			NewConnection.State = initialState;

			lock( m_Connections.SyncRoot )
			{
				m_Connections.Add( NewConnection );
			}
			
			return NewConnection;
		}	
		
		
		/// <summary>
		/// Creates a new <see cref="TcpConnection"/>.
		/// </summary>
		/// <param name="client">The client IP address.</param>
		/// <param name="clientSequenceNumber">The client's next sequence 
		/// number.</param>
		/// <param name="server">The server IP address.</param>
		/// <param name="serverSequenceNumber">The server's next sequence
		/// number.</param>
		/// <param name="initialState">Initial connection state.</param>
		/// <returns>A new Tcp connection.</returns>
		private TcpConnection CreateMissedTcpConnection( IPEndPoint client, 
		                                              uint clientSequenceNumber, 
		                                              IPEndPoint server, 
		                                              uint serverSequenceNumber, 
		                                              TcpState initialState )
		{
			TcpConnection NewConnection = new TcpConnection( client, clientSequenceNumber, server, serverSequenceNumber );
			
			// Hook up events.
			NewConnection.BytesTransferred += new TcpCommunicationEventHandler( OnMissedConnectionBytesTransferred );
			NewConnection.Disconnected += new TcpCommunicationEventHandler( OnMissedConnectionDisconnected );
			
			// Set initial connection state.
			NewConnection.State = initialState;

			lock( m_Connections.SyncRoot )
			{
				m_Connections.Add( NewConnection );
			}
			
			return NewConnection;
		}
		
		/// <summary>
		/// Processes the client's data even though we did not
		/// receive a connection.
		/// </summary>
		/// <param name="packet">A tcp packet.</param>
		private void ProcessMissedConnection( TcpPacket packet )
		{
			if( packet.GetDataBytes( ).Length > 0 )
			{						
				TcpConnection NewConnection = null;
				
				if( packet.DestinationAddress.ToString( ) == m_PacketFilter.RemoteEndPoint )
				{
					NewConnection = 
						CreateMissedTcpConnection( packet.SourceAddress, 
						                       packet.SequenceNumber, 
						                       packet.DestinationAddress, 
						                       packet.AcknowledgementNumber, 
						                       TcpState.SynReceived );
				}
				else
				{
					NewConnection = 
						CreateMissedTcpConnection( packet.DestinationAddress, 
					                           packet.AcknowledgementNumber, 
					                           packet.SourceAddress,
					                           packet.SequenceNumber,
					                           TcpState.SynReceived );						
				}
				
				NewConnection.ProcessPacket( packet );
			}
		}
		
		/// <summary>
		/// Determines whether the user of this class is interested in 
		/// data bytes from a client where the initial connection was missed.
		/// </summary>
		private bool IsMissedConnectionBytesWanted
		{
			get
			{
				bool Wanted = false;
				
				if( MissedConnectionBytesTransferred != null )
				{
					Wanted = true;
				}
				
				return Wanted;
			}
		}
	}
}
